package com.hexacta.hrs.domain.staffing.relationship;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;

/**
 * Se encarga de fusionar aquellos relationships que son similares.
 * 
 * @author ncortinez
 * 
 */
public class RelationshipEnsamblador {

    /**
     * Uno todos aquellos objetos del tipo RequirementAssignmentRelationship que
     * sean similares.
     * 
     * @param relationships
     *            : lista de relationships a unir.
     */
    public static void unirRelationships(final List<RequirementAssignmentRelationship> relationships) {

        unirRelationshipsPorPorcentaje(relationships);

        unirRelationshipsCorrelativos(relationships);
    }

    /**
     * Uno todos aquellos objetos del tipo RequirementAssignmentRelationship que
     * sean similares y cuyo rango de fechas sea correlativo. Por objetos
     * similares nos referimos a aquellos que son identicos a excepcion del
     * rango de fechas.
     * 
     * Ejemplo: si la lista contiene dos objetos cuyos rangos de fechas son
     * [01/01/2010,10/01/2010] y [11/01/2010,20/01/2010] respectivamente, el
     * metodo coloca en la lista un nuevo objeto con el rango horario
     * [01/01/2010,20/01/2010], y elimina los otros dos.
     * 
     * @param relationships
     *            : lista de relationships a unir.
     */
    public static void unirRelationshipsCorrelativos(final List<RequirementAssignmentRelationship> relationships) {

        boolean volverARecorrer = false;

        for (int i = 0; !volverARecorrer && i < relationships.size(); i++) {
            RequirementAssignmentRelationship asignacion = relationships.get(i);

            RequirementAssignmentRelationship correlativa = (RequirementAssignmentRelationship) CollectionUtils.find(
                    relationships, new CorrelativaPorIzquierda(asignacion));
            if (correlativa != null) {
                // La agrego a la lista de asignaciones a revisar y elimino las
                // dos originales.
                relationships.remove(asignacion);
                relationships.remove(correlativa);
                asignacion.setStartDate((Calendar) correlativa.getStartDate().clone());
                relationships.add(asignacion);

                // Vuelvo a recorrer la lista desde cero.
                volverARecorrer = true;
            }

            // Busco una parecida, en la que la fecha de inicio sea la siguiente
            // a mi fecha de fin.
            correlativa = null;
            correlativa = (RequirementAssignmentRelationship) CollectionUtils.find(relationships,
                    new CorrelativaPorDerecha(asignacion));
            if (correlativa != null) {
                // La agrego a la lista de asignaciones a revisar y elimino las
                // dos originales.
                relationships.remove(asignacion);
                relationships.remove(correlativa);
                asignacion.setEndDate((Calendar) correlativa.getEndDate().clone());
                relationships.add(asignacion);

                // Vuelvo a recorrer la lista desde cero.
                volverARecorrer = true;
            }

        }

        if (volverARecorrer) {
            unirRelationshipsCorrelativos(relationships);
        }
    }

    /**
     * Uno todos aquellos objetos del tipo RequirementAssignmentRelationship que
     * sean identicos, a excepcion de su porcentaje de asignacion
     * (assignmentPorcentage) En este caso, los dos objetos se unen en uno solo,
     * el cual contiene los porcentajes sumados.
     * 
     * @param relationships
     *            : lista de relationships a unir.
     */
    public static void unirRelationshipsPorPorcentaje(final List<RequirementAssignmentRelationship> relationships) {

        boolean volverARecorrer = true;

        // Busco aquellas relaciones del mismo empleado que se superponen en
        // fechas, y uno sus porcentajes.
        while (volverARecorrer) {
            volverARecorrer = false;

            for (int i = 0; i < relationships.size(); i++) {
                RequirementAssignmentRelationship curr = relationships.get(i);
                RequirementAssignmentRelationship intersect = findFirstIntersect(relationships, curr);

                if (intersect != null) {
                    volverARecorrer = true;
                    relationships.remove(curr);
                    relationships.remove(intersect);

                    relationships.addAll(getPieces(curr, intersect));
                }
            }

        }
    }

    private static Collection<? extends RequirementAssignmentRelationship> getPieces(
            final RequirementAssignmentRelationship curr, final RequirementAssignmentRelationship intersect) {

        // TODO: Hacer si no funciona con el particionador.
        List<RequirementAssignmentRelationship> particion = new ArrayList<RequirementAssignmentRelationship>();
        RequirementAssignmentRelationship anterior = new RequirementAssignmentRelationship();
        RequirementAssignmentRelationship posterior = new RequirementAssignmentRelationship();
        RequirementAssignmentRelationship intersectado = new RequirementAssignmentRelationship();
        Calendar st;
        Calendar end;

        // obtengo el fragmento anterior a la intersecci�n
        if (curr.getStartDate().before(intersect.getStartDate())) {

            st = (Calendar) intersect.getStartDate().clone();
            try {
                anterior = (RequirementAssignmentRelationship) curr.clone();
                anterior.setEndDate((Calendar) intersect.getStartDate().clone());
                anterior.getEndDate().add(Calendar.DAY_OF_YEAR, -1);

                particion.add(anterior);
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }

        } else if (curr.getStartDate().after(intersect.getStartDate())) {

            st = (Calendar) curr.getStartDate().clone();
            try {
                anterior = (RequirementAssignmentRelationship) intersect.clone();
                anterior.setEndDate((Calendar) curr.getStartDate().clone());
                anterior.getEndDate().add(Calendar.DAY_OF_YEAR, -1);

                particion.add(anterior);
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }

        } else {
            st = curr.getStartDate();
        }

        // obtengo el fragmento posterior a la intersecci�n
        if (curr.getEndDate().before(intersect.getEndDate())) {
            end = (Calendar) curr.getEndDate().clone();

            try {
                posterior = (RequirementAssignmentRelationship) intersect.clone();
                posterior.setStartDate((Calendar) curr.getEndDate().clone());
                posterior.getStartDate().add(Calendar.DAY_OF_YEAR, +1);

                particion.add(posterior);
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }

        } else if (curr.getEndDate().after(intersect.getEndDate())) {
            end = (Calendar) intersect.getEndDate().clone();

            try {
                posterior = (RequirementAssignmentRelationship) curr.clone();
                posterior.setStartDate((Calendar) intersect.getEndDate().clone());
                posterior.getStartDate().add(Calendar.DAY_OF_YEAR, +1);

                particion.add(posterior);
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }

        } else {
            end = curr.getEndDate();
        }

        // preparo el fragmento donde se intersectan

        try {
            intersectado = (RequirementAssignmentRelationship) curr.clone();
            intersectado.setStartDate(st);
            intersectado.setEndDate(end);
            intersectado.setPorcentage(curr.getPorcentage() + intersect.getPorcentage());

            particion.add(intersectado);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

        // particion.add()

        return particion;
    }

    private static RequirementAssignmentRelationship findFirstIntersect(
            final List<RequirementAssignmentRelationship> relationships, final RequirementAssignmentRelationship curr) {
        for (RequirementAssignmentRelationship rar : relationships) {
            if (rar.getEndDate().compareTo(curr.getStartDate()) >= 0
                    && curr.getEndDate().compareTo(rar.getStartDate()) >= 0 && curr != rar && curr.customEquals(rar))
                return rar;
        }
        return null;
    }
}

/**
 * La comparacion es cierta cuando el objeto pasado por parametro tiene como
 * fecha de inicio el dia inmediatamente posterior a la fecha de fin del objeto
 * base. El objeto base es el objeto pasado por el contructor.
 * 
 * @author ncortinez
 * 
 */
class CorrelativaPorDerecha implements Predicate {

    private RequirementAssignmentRelationship relationship;

    public CorrelativaPorDerecha(final RequirementAssignmentRelationship relationship) {
        this.relationship = relationship;
    }

    @Override
    public boolean evaluate(final Object arg) {
        RequirementAssignmentRelationship relation = (RequirementAssignmentRelationship) arg;

        // Es similiar si corresponde al mismo empleado y rol.
        boolean esSimilar = relation.getPorcentage() == relationship.getPorcentage()
                && relation.getEmployee().equals(relationship.getEmployee())
                && relation.getRole().getDescription().equals(relationship.getRole().getDescription());
        if (!esSimilar)
            return false;

        // Es correlativa por derecha si su fecha de inicio es el dia siguiente
        // a la fecha de fin del objeto base.
        Calendar diaInicioCorrelativa = (Calendar) relationship.getEndDate().clone();
        diaInicioCorrelativa.add(Calendar.DAY_OF_YEAR, 1);
        boolean esCorrelativa = relation.getStartDate().compareTo(diaInicioCorrelativa) == 0;
        if (!esCorrelativa)
            return false;

        return esSimilar && esCorrelativa;
    }
}

/**
 * La comparacion es cierta cuando el objeto pasado por parametro tiene como
 * fecha de fin el dia inmediatamente anterior a la fecha de inicio del objeto
 * base. El objeto base es el objeto pasado por el contructor.
 * 
 * @author ncortinez
 * 
 */
class CorrelativaPorIzquierda implements Predicate {

    private RequirementAssignmentRelationship relationship;

    public CorrelativaPorIzquierda(final RequirementAssignmentRelationship relationship) {
        this.relationship = relationship;
    }

    @Override
    public boolean evaluate(final Object arg) {
        RequirementAssignmentRelationship relation = (RequirementAssignmentRelationship) arg;

        // Es similiar si corresponde al mismo empleado y rol.
        boolean esSimilar = relation.getPorcentage() == relationship.getPorcentage()
                && relation.getEmployee().equals(relationship.getEmployee())
                && relation.getRole().getDescription().equals(relationship.getRole().getDescription());

        if (!esSimilar)
            return false;

        // Es correlativa por izquierda si su fecha de fin es el dia anterior a
        // la fecha de inicio del objeto base.
        Calendar diaFinCorrelativa = (Calendar) relationship.getStartDate().clone();
        diaFinCorrelativa.add(Calendar.DAY_OF_YEAR, -1);
        boolean esCorrelativa = relation.getEndDate().compareTo(diaFinCorrelativa) == 0;
        if (!esCorrelativa)
            return false;

        return esSimilar && esCorrelativa;
    }
}

/**
 * La comparacion es cierta cuando el intervalo de fechas [startDate,endDate]
 * del objeto pasado por parametro intersecta el intervalo de fechas del objeto
 * base. El objeto base es el objeto pasado por el contructor.
 * 
 * @author ncortinez
 * 
 */
class RelationshipIncluidaEnOtra implements Predicate {

    private RequirementAssignmentRelationship baseRelationship;

    public RelationshipIncluidaEnOtra(final RequirementAssignmentRelationship relationship) {
        baseRelationship = relationship;
    }

    @Override
    public boolean evaluate(final Object arg) {
        RequirementAssignmentRelationship incluida = (RequirementAssignmentRelationship) arg;

        if (!incluida.getRole().getDescription().equals(baseRelationship.getRole().getDescription()))
            return false;
        if (!incluida.getEmployee().equals(baseRelationship.getEmployee()))
            return false;

        boolean startDateIncluido = incluida.getStartDate().compareTo(baseRelationship.getStartDate()) >= 0
                && incluida.getStartDate().compareTo(baseRelationship.getEndDate()) <= 0;
        boolean endDateIncluido = incluida.getEndDate().compareTo(baseRelationship.getEndDate()) <= 0
                && incluida.getEndDate().compareTo(baseRelationship.getStartDate()) >= 0;

        return startDateIncluido || endDateIncluido;
    }
}
